{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Exercise 1 - Solution"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The first thing I am going to do is add an `as_dict` method to both my classes to make serialization a bit easier:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Stock:\n",
    "    def __init__(self, symbol, date_, open_, high, low, close, volume):\n",
    "        self.symbol = symbol\n",
    "        self.date = date_\n",
    "        self.open = open_\n",
    "        self.high = high\n",
    "        self.low = low\n",
    "        self.close = close\n",
    "        self.volume = volume\n",
    "        \n",
    "    def as_dict(self):\n",
    "        return dict(symbol=self.symbol, \n",
    "                    date=self.date,\n",
    "                    open=self.open,\n",
    "                    high=self.high,\n",
    "                    low=self.low,\n",
    "                    close=self.close,\n",
    "                    volume=self.volume)\n",
    "        \n",
    "class Trade:\n",
    "    def __init__(self, symbol, timestamp, order, price, volume, commission):\n",
    "        self.symbol = symbol\n",
    "        self.timestamp = timestamp\n",
    "        self.order = order\n",
    "        self.price = price\n",
    "        self.commission = commission\n",
    "        self.volume = volume\n",
    "        \n",
    "    def as_dict(self):\n",
    "        return dict(\n",
    "            symbol=self.symbol,\n",
    "            timestamp=self.timestamp,\n",
    "            order=self.order,\n",
    "            price=self.price,\n",
    "            volume=self.volume,\n",
    "            commission=self.commission)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datetime import date, datetime\n",
    "from decimal import Decimal\n",
    "\n",
    "activity = {\n",
    "    \"quotes\": [\n",
    "        Stock('TSLA', date(2018, 11, 22), \n",
    "              Decimal('338.19'), Decimal('338.64'), Decimal('337.60'), Decimal('338.19'), 365_607),\n",
    "        Stock('AAPL', date(2018, 11, 22), \n",
    "              Decimal('176.66'), Decimal('177.25'), Decimal('176.64'), Decimal('176.78'), 3_699_184),\n",
    "        Stock('MSFT', date(2018, 11, 22), \n",
    "              Decimal('103.25'), Decimal('103.48'), Decimal('103.07'), Decimal('103.11'), 4_493_689)\n",
    "    ],\n",
    "    \n",
    "    \"trades\": [\n",
    "        Trade('TSLA', datetime(2018, 11, 22, 10, 5, 12), 'buy', Decimal('338.25'), 100, Decimal('9.99')),\n",
    "        Trade('AAPL', datetime(2018, 11, 22, 10, 30, 5), 'sell', Decimal('177.01'), 20, Decimal('9.99'))\n",
    "    ]\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "My approach is going to be to serialize these classes using a special class name identifier.\n",
    "\n",
    "For example to serialize `Stock` objects I will use this format:\n",
    "\n",
    "```\n",
    "{\n",
    "    \"object\": \"Stock\",\n",
    "    \"symbol\": \"...\",\n",
    "    ...\n",
    "}\n",
    "```\n",
    "\n",
    "Similarly for a `Trade` objects."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Furthermore, I need to pay special attention to dates, timestamps and prices.\n",
    "\n",
    "For dates and timestamps I will use the standard ISO format (`YYYY-MM-DD` and `YYYY-MM-DDTHH:MM:SS`).\n",
    "\n",
    "Prices are stored in `Decimal` objects - so we'll have to handle serialization for those objects too."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from json import JSONEncoder, dumps\n",
    "\n",
    "class CustomEncoder(JSONEncoder):\n",
    "    def default(self, obj):\n",
    "        if isinstance(obj, Stock):\n",
    "            return obj.as_dict()\n",
    "        elif isinstance(obj, Trade):\n",
    "            return obj_as_dict()\n",
    "        else:\n",
    "            super().default(obj)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This will not work quite yet - we are not handling decimal, date and datetime serialization:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "Object of type 'date' is not JSON serializable",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-4-43e91b3b7717>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mdumps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mactivity\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcls\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mCustomEncoder\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;32m~/anaconda3/envs/deepdive/lib/python3.6/json/__init__.py\u001b[0m in \u001b[0;36mdumps\u001b[0;34m(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)\u001b[0m\n\u001b[1;32m    236\u001b[0m         \u001b[0mcheck_circular\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcheck_circular\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mallow_nan\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mallow_nan\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindent\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mindent\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    237\u001b[0m         \u001b[0mseparators\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mseparators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdefault\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdefault\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msort_keys\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msort_keys\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 238\u001b[0;31m         **kw).encode(obj)\n\u001b[0m\u001b[1;32m    239\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    240\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;32m~/anaconda3/envs/deepdive/lib/python3.6/json/encoder.py\u001b[0m in \u001b[0;36mencode\u001b[0;34m(self, o)\u001b[0m\n\u001b[1;32m    197\u001b[0m         \u001b[0;31m# exceptions aren't as detailed.  The list call should be roughly\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    198\u001b[0m         \u001b[0;31m# equivalent to the PySequence_Fast that ''.join() would do.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 199\u001b[0;31m         \u001b[0mchunks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0miterencode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mo\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m_one_shot\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    200\u001b[0m         \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mchunks\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mlist\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtuple\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    201\u001b[0m             \u001b[0mchunks\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mchunks\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;32m~/anaconda3/envs/deepdive/lib/python3.6/json/encoder.py\u001b[0m in \u001b[0;36miterencode\u001b[0;34m(self, o, _one_shot)\u001b[0m\n\u001b[1;32m    255\u001b[0m                 \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkey_separator\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mitem_separator\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msort_keys\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    256\u001b[0m                 self.skipkeys, _one_shot)\n\u001b[0;32m--> 257\u001b[0;31m         \u001b[0;32mreturn\u001b[0m \u001b[0m_iterencode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mo\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m    258\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    259\u001b[0m def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,\n",
      "\u001b[0;32m<ipython-input-3-e681e72edf6d>\u001b[0m in \u001b[0;36mdefault\u001b[0;34m(self, obj)\u001b[0m\n\u001b[1;32m      8\u001b[0m             \u001b[0;32mreturn\u001b[0m \u001b[0mobj_as_dict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      9\u001b[0m         \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 10\u001b[0;31m             \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdefault\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;32m~/anaconda3/envs/deepdive/lib/python3.6/json/encoder.py\u001b[0m in \u001b[0;36mdefault\u001b[0;34m(self, o)\u001b[0m\n\u001b[1;32m    178\u001b[0m         \"\"\"\n\u001b[1;32m    179\u001b[0m         raise TypeError(\"Object of type '%s' is not JSON serializable\" %\n\u001b[0;32m--> 180\u001b[0;31m                         o.__class__.__name__)\n\u001b[0m\u001b[1;32m    181\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m    182\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0mencode\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mo\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mTypeError\u001b[0m: Object of type 'date' is not JSON serializable"
     ]
    }
   ],
   "source": [
    "dumps(activity, cls=CustomEncoder)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There's a few ways we can fix that - we could serialize by coding the date formatting directly in the `Trade` or `Stock` serializers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from json import JSONEncoder, dumps\n",
    "\n",
    "class CustomEncoder(JSONEncoder):\n",
    "    def default(self, obj):\n",
    "        if isinstance(obj, Stock):\n",
    "            result = obj.as_dict()\n",
    "            result['date'] = result['date'].strftime('%Y-%m-%d')\n",
    "            return result\n",
    "        elif isinstance(obj, Trade):\n",
    "            result = obj.as_dict()\n",
    "            result['timestamp'] = result['timestamp'].strftime('%Y-%m-%dT%H:%M:%S')\n",
    "            return result\n",
    "        else:\n",
    "            super().default(obj)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This will still not quite work because we are not handling serizliation of `Decimal` objects. But I would rather not have to handle them the way we are handling `date` and `datetime` objects - that would be very tedious.\n",
    "\n",
    "In fact, I am going to write handlers for the `Decimal` as well as `date` and `datetime` classes this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from json import JSONEncoder, dumps\n",
    "\n",
    "class CustomEncoder(JSONEncoder):\n",
    "    def default(self, obj):\n",
    "        if isinstance(obj, Stock) or isinstance(obj, Trade):\n",
    "            return obj.as_dict()\n",
    "        elif isinstance(obj, datetime):\n",
    "            # check for datetime first, because a datetime is also a date\n",
    "            return obj.strftime('%Y-%m-%dT%H:%M:%S')\n",
    "        elif isinstance(obj, date):\n",
    "            return obj.strftime('%Y-%m-%d')\n",
    "        elif isinstance(obj, Decimal):\n",
    "            return str(obj)\n",
    "        else:\n",
    "            super().default(obj)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "encoded = dumps(activity, cls=CustomEncoder, indent=2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "  \"quotes\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"338.19\",\n",
      "      \"high\": \"338.64\",\n",
      "      \"low\": \"337.60\",\n",
      "      \"close\": \"338.19\",\n",
      "      \"volume\": 365607\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"176.66\",\n",
      "      \"high\": \"177.25\",\n",
      "      \"low\": \"176.64\",\n",
      "      \"close\": \"176.78\",\n",
      "      \"volume\": 3699184\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"MSFT\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"103.25\",\n",
      "      \"high\": \"103.48\",\n",
      "      \"low\": \"103.07\",\n",
      "      \"close\": \"103.11\",\n",
      "      \"volume\": 4493689\n",
      "    }\n",
      "  ],\n",
      "  \"trades\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"timestamp\": \"2018-11-22T10:05:12\",\n",
      "      \"order\": \"buy\",\n",
      "      \"price\": \"338.25\",\n",
      "      \"volume\": 100,\n",
      "      \"commission\": \"9.99\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"timestamp\": \"2018-11-22T10:30:05\",\n",
      "      \"order\": \"sell\",\n",
      "      \"price\": \"177.01\",\n",
      "      \"volume\": 20,\n",
      "      \"commission\": \"9.99\"\n",
      "    }\n",
      "  ]\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "print(encoded)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We're almost there - the serialization works just fine, but if I'm going to deserialize the objects later, I will need to know what the object type is for the `Trade` and `Stock` objects. We could add it to the `as_dict` methods of each class, but I don't necessarily want it all the time - so instead I am going to inject the class name during the serialization:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CustomEncoder(JSONEncoder):\n",
    "    def default(self, obj):\n",
    "        if isinstance(obj, Stock) or isinstance(obj, Trade):\n",
    "            result =  obj.as_dict()\n",
    "            result['object'] = obj.__class__.__name__\n",
    "            return result\n",
    "        elif isinstance(obj, datetime):\n",
    "            return obj.strftime('%Y-%m-%dT%H:%M:%S')\n",
    "        elif isinstance(obj, date):\n",
    "            return obj.strftime('%Y-%m-%d')\n",
    "        elif isinstance(obj, Decimal):\n",
    "            return str(obj)\n",
    "        else:\n",
    "            super().default(obj)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{\n",
      "  \"quotes\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"338.19\",\n",
      "      \"high\": \"338.64\",\n",
      "      \"low\": \"337.60\",\n",
      "      \"close\": \"338.19\",\n",
      "      \"volume\": 365607,\n",
      "      \"object\": \"Stock\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"176.66\",\n",
      "      \"high\": \"177.25\",\n",
      "      \"low\": \"176.64\",\n",
      "      \"close\": \"176.78\",\n",
      "      \"volume\": 3699184,\n",
      "      \"object\": \"Stock\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"MSFT\",\n",
      "      \"date\": \"2018-11-22\",\n",
      "      \"open\": \"103.25\",\n",
      "      \"high\": \"103.48\",\n",
      "      \"low\": \"103.07\",\n",
      "      \"close\": \"103.11\",\n",
      "      \"volume\": 4493689,\n",
      "      \"object\": \"Stock\"\n",
      "    }\n",
      "  ],\n",
      "  \"trades\": [\n",
      "    {\n",
      "      \"symbol\": \"TSLA\",\n",
      "      \"timestamp\": \"2018-11-22T10:05:12\",\n",
      "      \"order\": \"buy\",\n",
      "      \"price\": \"338.25\",\n",
      "      \"volume\": 100,\n",
      "      \"commission\": \"9.99\",\n",
      "      \"object\": \"Trade\"\n",
      "    },\n",
      "    {\n",
      "      \"symbol\": \"AAPL\",\n",
      "      \"timestamp\": \"2018-11-22T10:30:05\",\n",
      "      \"order\": \"sell\",\n",
      "      \"price\": \"177.01\",\n",
      "      \"volume\": 20,\n",
      "      \"commission\": \"9.99\",\n",
      "      \"object\": \"Trade\"\n",
      "    }\n",
      "  ]\n",
      "}\n"
     ]
    }
   ],
   "source": [
    "result = dumps(activity, cls=CustomEncoder, indent=2)\n",
    "print(result)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
